#
# This module is a pure Python version of pypy.module.struct.
# It is only imported if the vastly faster pypy.module.struct is not
# compiled in.  For now we keep this version for reference and
# because pypy.module.struct is not ootype-backend-friendly yet.
#

# this module 'borrowed' from
# https://bitbucket.org/pypy/pypy/src/18626459a9b2/lib_pypy/_struct.py?at=py3k-listview_str
# with many bug fixes

"""Functions to convert between Python values and C structs.
Python strings are used to hold the data representing the C struct
and also as format strings to describe the layout of data in the C struct.

The optional first format char indicates byte order, size and alignment:
 @: native order, size & alignment (default)
 =: native order, std. size & alignment
 <: little-endian, std. size & alignment
 >: big-endian, std. size & alignment
 !: same as >

The remaining chars indicate types of args and must match exactly;
these can be preceded by a decimal repeat count:
   x: pad byte (no data);
   c:char;
   b:signed byte;
   B:unsigned byte;
   h:short;
   H:unsigned short;
   i:int;
   I:unsigned int;
   l:long;
   L:unsigned long;
   f:float;
   d:double.
Special cases (preceding decimal count indicates length):
   s:string (array of char); p: pascal string (with count byte).
Special case (only available in native format):
   P:an integer type that is wide enough to hold a pointer.
Special case (not in native mode unless 'long long' in platform C):
   q:long long;
   Q:unsigned long long
Whitespace between formats is ignored.

The variable struct.error is an exception raised on errors."""

import math
import re
import sys

# TODO: XXX Find a way to get information on native sizes and alignments
class StructError(Exception):
    pass


error = StructError

def _normalize(fmt):
    """Check if there are illegal whitespaces (between a count and its format)
    and remove other whitespaces."""
    if re.search(r"\d\s+", fmt):
        raise StructError("bad char in struct format")
    return fmt.replace(" ", "")

def unpack_int(data, index, size, le):
    bytes = [b for b in data[index:index+size]]
    if le == 'little':
        bytes.reverse()
    number = 0
    for b in bytes:
        number = number << 8 | b
    return int(number)

def unpack_signed_int(data, index, size, le):
    number = unpack_int(data, index, size, le)
    max = 2 ** (size * 8)
    if number > 2 ** (size * 8 - 1) - 1:
        number = int(-1 * (max - number))
    return number

INFINITY = 1e200 * 1e200
NAN = INFINITY / INFINITY

def unpack_char(data, index, size, le):
    return data[index:index + size]

def pack_int(number, size, le):
    x = number
    res = []
    for i in range(size):
        res.append(x&0xff)
        x >>= 8
    if le == 'big':
        res.reverse()
    return bytes(res)

def pack_signed_int(number, size, le):
    if not isinstance(number, int):
        raise StructError("argument for i,I,l,L,q,Q,h,H must be integer")
    if  number > 2 ** (8 * size - 1) - 1 or number < -1 * 2 ** (8 * size - 1):
        raise OverflowError("Number:%i too large to convert" % number)
    return pack_int(number, size, le)

def pack_unsigned_int(number, size, le):
    if not isinstance(number, int):
        raise StructError("argument for i,I,l,L,q,Q,h,H must be integer")
    if number < 0:
        raise TypeError("can't convert negative long to unsigned")
    if number > 2 ** (8 * size) - 1:
        raise OverflowError("Number:%i too large to convert" % number)
    return pack_int(number, size, le)

def pack_char(char, size, le):
    return bytes(char)

def isinf(x):
    return x != 0.0 and x / 2 == x

def isnan(v):
    return v != v * 1.0 or (v == 1.0 and v == 2.0)

def pack_float(x, size, le):
    unsigned = float_pack(x, size)
    result = []
    for i in range(size):
        result.append((unsigned >> (i * 8)) & 0xFF)
    if le == "big":
        result.reverse()
    return bytes(result)

def unpack_float(data, index, size, le):
    binary = [data[i] for i in range(index, index + size)]
    if le == "big":
        binary.reverse()
    unsigned = 0
    for i in range(size):
        unsigned |= binary[i] << (i * 8)
    return float_unpack(unsigned, size, le)

def round_to_nearest(x):
    """Python 3 style round:  round a float x to the nearest int, but
    unlike the builtin Python 2.x round function:

      - return an int, not a float
      - do round-half-to-even, not round-half-away-from-zero.

    We assume that x is finite and nonnegative; except wrong results
    if you use this for negative x.

    """
    int_part = int(x)
    frac_part = x - int_part
    if frac_part > 0.5 or frac_part == 0.5 and int_part & 1 == 1:
        int_part += 1
    return int_part

def float_unpack(Q, size, le):
    """Convert a 32-bit or 64-bit integer created
    by float_pack into a Python float."""

    if size == 8:
        MIN_EXP = -1021  # = sys.float_info.min_exp
        MAX_EXP = 1024   # = sys.float_info.max_exp
        MANT_DIG = 53    # = sys.float_info.mant_dig
        BITS = 64
    elif size == 4:
        MIN_EXP = -125   # C's FLT_MIN_EXP
        MAX_EXP = 128    # FLT_MAX_EXP
        MANT_DIG = 24    # FLT_MANT_DIG
        BITS = 32
    else:
        raise ValueError("invalid size value")

    if Q >> BITS:
         raise ValueError("input out of range")

    # extract pieces
    sign = Q >> BITS - 1
    exp = (Q & ((1 << BITS - 1) - (1 << MANT_DIG - 1))) >> MANT_DIG - 1
    mant = Q & ((1 << MANT_DIG - 1) - 1)

    if exp == MAX_EXP - MIN_EXP + 2:
        # nan or infinity
        result = float('nan') if mant else float('inf')
    elif exp == 0:
        # subnormal or zero
        result = math.ldexp(float(mant), MIN_EXP - MANT_DIG)
    else:
        # normal
        mant += 1 << MANT_DIG - 1
        result = math.ldexp(float(mant), exp + MIN_EXP - MANT_DIG - 1)
    return -result if sign else result


def float_pack(x, size):
    """Convert a Python float x into a 64-bit unsigned integer
    with the same byte representation."""

    if size == 8:
        MIN_EXP = -1021  # = sys.float_info.min_exp
        MAX_EXP = 1024   # = sys.float_info.max_exp
        MANT_DIG = 53    # = sys.float_info.mant_dig
        BITS = 64
    elif size == 4:
        MIN_EXP = -125   # C's FLT_MIN_EXP
        MAX_EXP = 128    # FLT_MAX_EXP
        MANT_DIG = 24    # FLT_MANT_DIG
        BITS = 32
    else:
        raise ValueError("invalid size value")

    sign = math.copysign(1.0, x) < 0.0
    if math.isinf(x):
        mant = 0
        exp = MAX_EXP - MIN_EXP + 2
    elif math.isnan(x):
        mant = 1 << (MANT_DIG-2) # other values possible
        exp = MAX_EXP - MIN_EXP + 2
    elif x == 0.0:
        mant = 0
        exp = 0
    else:
        m, e = math.frexp(abs(x))  # abs(x) == m * 2**e
        exp = e - (MIN_EXP - 1)
        if exp > 0:
            # Normal case.
            mant = round_to_nearest(m * (1 << MANT_DIG))
            mant -= 1 << MANT_DIG - 1
        else:
            # Subnormal case.
            if exp + MANT_DIG - 1 >= 0:
                mant = round_to_nearest(m * (1 << exp + MANT_DIG - 1))
            else:
                mant = 0
            exp = 0

        # Special case: rounding produced a MANT_DIG-bit mantissa.
        assert 0 <= mant <= 1 << MANT_DIG - 1
        if mant == 1 << MANT_DIG - 1:
            mant = 0
            exp += 1

        # Raise on overflow (in some circumstances, may want to return
        # infinity instead).
        if exp >= MAX_EXP - MIN_EXP + 2:
             raise OverflowError("float too large to pack in this format")

    # check constraints
    assert 0 <= mant < 1 << MANT_DIG - 1
    assert 0 <= exp <= MAX_EXP - MIN_EXP + 2
    assert 0 <= sign <= 1
    return ((sign << BITS - 1) | (exp << MANT_DIG - 1)) | mant


big_endian_format = {
    'x': {'size': 1, 'alignment': 0, 'pack': None, 'unpack': None},
    'b': {'size': 1, 'alignment': 0, 'pack': pack_signed_int, 'unpack': unpack_signed_int},
    'B': {'size': 1, 'alignment': 0, 'pack': pack_unsigned_int, 'unpack': unpack_int},
    'c': {'size': 1, 'alignment': 0, 'pack': pack_char, 'unpack': unpack_char},
    's': {'size': 1, 'alignment': 0, 'pack': None, 'unpack': None},
    'p': {'size': 1, 'alignment': 0, 'pack': None, 'unpack': None},
    'h': {'size': 2, 'alignment': 0, 'pack': pack_signed_int, 'unpack': unpack_signed_int},
    'H': {'size': 2, 'alignment': 0, 'pack': pack_unsigned_int, 'unpack': unpack_int},
    'i': {'size': 4, 'alignment': 0, 'pack': pack_signed_int, 'unpack': unpack_signed_int},
    'I': {'size': 4, 'alignment': 0, 'pack': pack_unsigned_int, 'unpack': unpack_int},
    'l': {'size': 4, 'alignment': 0, 'pack': pack_signed_int, 'unpack': unpack_signed_int},
    'L': {'size': 4, 'alignment': 0, 'pack': pack_unsigned_int, 'unpack': unpack_int},
    'q': {'size': 8, 'alignment': 0, 'pack': pack_signed_int, 'unpack': unpack_signed_int},
    'Q': {'size': 8, 'alignment': 0, 'pack': pack_unsigned_int, 'unpack': unpack_int},
    'f': {'size': 4, 'alignment': 0, 'pack': pack_float, 'unpack': unpack_float},
    'd': {'size': 8, 'alignment': 0, 'pack': pack_float, 'unpack': unpack_float},
    'P': {'size': 8, 'alignment': 0, 'pack': pack_unsigned_int, 'unpack': unpack_int}
    }

default = big_endian_format

formatmode = { '<' : (default, 'little'),
             '>' : (default, 'big'),
             '!' : (default, 'big'),
             '=' : (default, sys.byteorder),
             '@' : (default, sys.byteorder)
            }

def _getmode(fmt):
    try:
        formatdef, endianness = formatmode[fmt[0]]
        alignment = fmt[0] not in formatmode or fmt[0] == '@'
        index = 1
    except (IndexError, KeyError):
        formatdef, endianness = formatmode['@']
        alignment = True
        index = 0
    return formatdef, endianness, index, alignment

def _getnum(fmt, i):
    num = None
    cur = fmt[i]
    while ('0'<= cur ) and ( cur <= '9'):
        if num == None:
            num = int(cur)
        else:
            num = 10 * num + int(cur)
        i += 1
        cur = fmt[i]
    return num, i

def calcsize(fmt):
    """calcsize(fmt) -> int
    Return size of C struct described by format string fmt.
    See struct.__doc__ for more on format strings."""
    if isinstance(fmt, bytes):
        fmt = fmt.decode("ascii")

    fmt = _normalize(fmt)

    formatdef, endianness, i, alignment = _getmode(fmt)
    num = 0
    result = 0
    while i < len(fmt):
        num, i = _getnum(fmt,i)
        cur = fmt[i]
        try:
            format = formatdef[cur]
        except KeyError:
            raise StructError("%s is not a valid format" % cur)
        if num != None :
            result += num * format['size']
        else:
            # if formatdef is native, alignment is native, so we count a
            # number of padding bytes until result is a multiple of size
            if alignment and result:
                result += format['size'] - result % format['size']
            result += format['size']
        num = 0
        i += 1
    return result

def pack(fmt, *args):
    """pack(fmt, v1, v2, ...) -> string
       Return string containing values v1, v2, ... packed according to fmt.
       See struct.__doc__ for more on format strings."""
    fmt = _normalize(fmt)
    formatdef, endianness, i, alignment = _getmode(fmt)
    args = list(args)
    n_args = len(args)
    result = []
    while i < len(fmt):
        num, i = _getnum(fmt, i)
        cur = fmt[i]
        try:
            format = formatdef[cur]
        except KeyError:
            raise StructError("%s is not a valid format" % cur)
        if num == None :
            num_s = 0
            num = 1
        else:
            num_s = num

        if cur == 'x':
            result += [b'\0' * num]
        elif cur == 's':
            if isinstance(args[0], bytes):
                padding = num - len(args[0])
                result += [args[0][:num] + b'\0' * padding]
                args.pop(0)
            else:
                raise StructError("arg for string format not a string")
        elif cur == 'p':
            if isinstance(args[0], bytes):
                padding = num - len(args[0]) - 1

                if padding > 0:
                    result += [bytes([len(args[0])]) + args[0][:num-1] +
                        b'\0'*padding]
                else:
                    if num < 255:
                        result += [bytes([num-1]) + args[0][:num - 1]]
                    else:
                        result += [bytes([255]) + args[0][:num - 1]]
                args.pop(0)
            else:
                raise StructError("arg for string format not a string")

        else:
            if len(args) < num:
                raise StructError("insufficient arguments to pack")
            for var in args[:num]:
                # pad with 0 until position is a multiple of size
                if len(result) and alignment:
                    padding = format['size'] - len(result) % format['size']
                    result += [bytes([0])] * padding
                result += [format['pack'](var, format['size'], endianness)]
            args = args[num:]
        num = None
        i += 1
    if len(args) != 0:
        raise StructError("too many arguments for pack format")
    return b''.join(result)

def unpack(fmt, data):
    """unpack(fmt, string) -> (v1, v2, ...)
       Unpack the string, containing packed C structure data, according
       to fmt.  Requires len(string)==calcsize(fmt).
       See struct.__doc__ for more on format strings."""
    fmt = _normalize(fmt)
    formatdef, endianness, i, alignment = _getmode(fmt)
    j = 0
    num = 0
    result = []
    length = calcsize(fmt)
    if length != len (data):
        raise StructError("unpack str size does not match format")
    while i < len(fmt):
        num, i = _getnum(fmt, i)
        cur = fmt[i]
        i += 1
        try:
            format = formatdef[cur]
        except KeyError:
            raise StructError("%s is not a valid format" % cur)

        if not num :
            num = 1

        if cur == 'x':
            j += num
        elif cur == 's':
            result.append(data[j:j + num])
            j += num
        elif cur == 'p':
            n = data[j]
            if n >= num:
                n = num - 1
            result.append(data[j + 1:j + n + 1])
            j += num
        else:
            # skip padding bytes until we get at a multiple of size
            if j > 0 and alignment:
                padding = format['size'] - j % format['size']
                j += padding
            for n in range(num):
                result += [format['unpack'](data, j, format['size'],
                    endianness)]
                j += format['size']

    return tuple(result)

def pack_into(fmt, buf, offset, *args):
    data = pack(fmt, *args)
    buf[offset:offset + len(data)] = data

def unpack_from(fmt, buf, offset=0):
    size = calcsize(fmt)
    data = buf[offset:offset + size]
    if len(data) != size:
        raise error("unpack_from requires a buffer of at least %d bytes"
                    % (size,))
    return unpack(fmt, data)

def _clearcache():
    "Clear the internal cache."
    # No cache in this implementation

class Struct:

    def __init__(self, fmt):
        self.format = fmt

    def pack(self, *args):
        return pack(self.format, *args)

    def pack_into(self, *args):
        return pack_into(self.format, *args)

    def unpack(self, *args):
        return unpack(self.format, *args)

    def unpack_from(self, *args):
        return unpack_from(self.format, *args)

if __name__ == '__main__':
    t = pack('Bf', 1, 2)
    print(t, len(t))
    print(unpack('Bf', t))
    print(calcsize('Bf'))

